// Copyright (c) 2011 Blue Onion Software, All rights reserved
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Web;
using System.Web.Hosting;
using System.Web.Security;

namespace BlueOnionSoftware.Bloget.Providers
{
    public class XmlMembershipProvider : MembershipProvider
    {
        private string dataStorePath;
        private string applicationName;
        private bool enableReset;
        private bool enableRetrieve;
        private MembershipPasswordFormat passwordFormat;
        private bool uniqueEmail;
        private string passwordExpression;
        private bool requiresChallenge;
        private int attempts;
        private int attemptWindow;
        private int minPasswordLength;
        private int minNonAlphaChars;
        private readonly Encoding encoder = Encoding.UTF8;

        public string DataStorePath
        {
            get { return dataStorePath; }
            set { dataStorePath = value; }
        }

        private string DataStoreFile
        {
            get
            {
                string path;

                if (HttpContext.Current != null)
                {
                    path = HttpContext.Current.Server.MapPath(DataStorePath);
                }

                else
                {
                    path = HostingEnvironment.ApplicationPhysicalPath;
                    var dataStorePath = DataStorePath.Replace("~/", string.Empty);
                    path = Path.Combine(path, dataStorePath);
                }

                return Path.Combine(path, string.Format("{0}_Members.xml", applicationName));
            }
        }

        private Data.UserCollection RetrieveUsers(bool activeOnly)
        {
            var users = Data.UserCollection.Load(DataStoreFile);
            return (activeOnly) ? users.FindAllActiveUsers() : users;
        }

        private static byte[] ComputeHash(byte[] input)
        {
            using (var provider = new SHA512Managed())
            {
                var hash = provider.ComputeHash(input);
                provider.Clear();
                return hash;
            }
        }

        private static MembershipUserCollection CreateMembershipUserCollection(IList<Data.User> users)
        {
            var userCollection = new MembershipUserCollection();

            foreach (var user in users)
            {
                userCollection.Add(user.CreateMembershipUser(Membership.Provider.Name));
            }

            return userCollection;
        }

        private bool CompareByteArray(byte[] array1, byte[] array2)
        {
            if (array1.Length != array2.Length)
            {
                return false;
            }

            for (var i = 0; i < array1.Length; ++i)
            {
                if (array1[i] != array2[i])
                {
                    return false;
                }
            }

            return true;
        }

        public override void Initialize(string name, NameValueCollection config)
        {
            Throw.IfNull(config, "config");

            if (String.IsNullOrEmpty(name))
            {
                name = "XmlMembershipProvider";
            }

            base.Initialize(name, config);

            passwordFormat = String.IsNullOrEmpty(config["passwordFormat"])
                ? MembershipPasswordFormat.Hashed
                : passwordFormat = (MembershipPasswordFormat) Enum.Parse(typeof (MembershipPasswordFormat), config["passwordFormat"], true);

            dataStorePath = String.IsNullOrEmpty(config["dataStorePath"]) ? "~/App_Data" : config["dataStorePath"];
            applicationName = String.IsNullOrEmpty(config["applicationName"]) ? "Bloget" : config["applicationName"];
            enableRetrieve = String.IsNullOrEmpty(config["enablePasswordRetrieval"]) ? false : bool.Parse(config["enablePasswordRetrieval"]);
            enableReset = String.IsNullOrEmpty(config["enablePasswordReset"]) ? false : bool.Parse(config["enablePasswordReset"]);
            requiresChallenge = String.IsNullOrEmpty(config["requiresQuestionAndAnswer"]) ? false : bool.Parse(config["requiresQuestionAndAnswer"]);
            uniqueEmail = String.IsNullOrEmpty(config["requiresUniqueEmail"]) ? false : bool.Parse(config["requiresUniqueEmail"]);
            attemptWindow = String.IsNullOrEmpty(config["passwordAttemptWindow"]) ? 1 : int.Parse(config["passwordAttemptWindow"], CultureInfo.InvariantCulture);
            passwordExpression = String.IsNullOrEmpty(config["passwordStrengthRegularExpression"]) ? string.Empty : config["passwordStrengthRegularExpression"];
            attempts = String.IsNullOrEmpty(config["maxInvalidPasswordAttempts"]) ? 6 : int.Parse(config["maxInvalidPasswordAttempts"], CultureInfo.InvariantCulture);
            minNonAlphaChars = String.IsNullOrEmpty(config["minRequiredNonAlphanumericCharacters"]) ? 0 : int.Parse(config["minRequiredNonAlphanumericCharacters"], CultureInfo.InvariantCulture);
            minPasswordLength = String.IsNullOrEmpty(config["minRequiredPasswordLength"]) ? 4 : int.Parse(config["minRequiredPasswordLength"], CultureInfo.InvariantCulture);
        }

        public override string ApplicationName
        {
            get { return applicationName; }
            set { applicationName = value; }
        }

        public override bool ChangePassword(string username, string oldPassword, string newPassword)
        {
            var users = RetrieveUsers(true);
            var user = users.FindByUserName(username);

            if (user == null)
            {
                return false;
            }

            var oldPass = encoder.GetBytes(oldPassword);
            var newPass = encoder.GetBytes(newPassword);

            switch (PasswordFormat)
            {
                case MembershipPasswordFormat.Clear:
                    if (CompareByteArray(user.Password, oldPass))
                    {
                        user.Password = newPass;
                    }

                    else
                    {
                        return false;
                    }
                    break;

                case MembershipPasswordFormat.Encrypted:
                    if (CompareByteArray(user.Password, base.EncryptPassword(oldPass)))
                    {
                        user.Password = base.EncryptPassword(newPass);
                    }

                    else
                    {
                        return false;
                    }
                    break;

                case MembershipPasswordFormat.Hashed:
                    if (CompareByteArray(user.Password, ComputeHash(oldPass)))
                    {
                        user.Password = ComputeHash(newPass);
                    }

                    else
                    {
                        return false;
                    }
                    break;
            }

            users.Save(DataStoreFile);
            return true;
        }

        public override bool ChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer)
        {
            var users = RetrieveUsers(true);
            var usr = users.FindByUserName(username);

            if (usr == null)
            {
                return false;
            }

            var passwordOK = false;
            var passBytes = encoder.GetBytes(password);

            switch (PasswordFormat)
            {
                case MembershipPasswordFormat.Clear:
                    passwordOK = CompareByteArray(usr.Password, passBytes);
                    break;

                case MembershipPasswordFormat.Encrypted:
                    passwordOK = CompareByteArray(usr.Password, base.EncryptPassword(passBytes));
                    break;

                case MembershipPasswordFormat.Hashed:
                    passwordOK = CompareByteArray(usr.Password, ComputeHash(passBytes));
                    break;
            }

            if (passwordOK)
            {
                usr.ChallengeQuestion = newPasswordQuestion;
                usr.ChallengeAnswer = newPasswordAnswer;
                users.Save(DataStoreFile);
            }

            return passwordOK;
        }

        public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status)
        {
            var users = RetrieveUsers(false);

            //Perform Checks
            if (users.FindByUserName(username) != null)
            {
                status = MembershipCreateStatus.DuplicateUserName;
                return null;
            }

            if (RequiresUniqueEmail && users.FindByEmail(email) != null)
            {
                status = MembershipCreateStatus.DuplicateEmail;
                return null;
            }

            var newUser = new Data.User();
            var pass = encoder.GetBytes(password);
            newUser.Key = Guid.NewGuid();
            newUser.UserName = username;

            switch (PasswordFormat)
            {
                case MembershipPasswordFormat.Clear:
                    newUser.Password = pass;
                    break;

                case MembershipPasswordFormat.Hashed:
                    newUser.Password = ComputeHash(pass);
                    break;

                case MembershipPasswordFormat.Encrypted:
                    newUser.Password = base.EncryptPassword(pass);
                    break;
            }

            newUser.CreatedOn = DateTime.UtcNow;
            newUser.LastLoggedIn = DateTime.UtcNow;
            newUser.LastActivity = DateTime.UtcNow;
            newUser.IsActive = isApproved;
            newUser.Email = email;
            newUser.ChallengeQuestion = passwordQuestion;
            newUser.ChallengeAnswer = passwordAnswer;

            users.Add(newUser);
            users.Save(DataStoreFile);

            status = MembershipCreateStatus.Success;
            return newUser.CreateMembershipUser(Membership.Provider.Name);
        }

        public override bool DeleteUser(string username, bool deleteAllRelatedData)
        {
            var users = RetrieveUsers(false);
            var usr = users.FindByUserName(username);

            if (usr == null)
            {
                return false;
            }

            if (deleteAllRelatedData)
            {
                users.Remove(usr);
            }

            else
            {
                usr.IsActive = false;
            }

            users.Save(DataStoreFile);
            return true;
        }

        public override bool EnablePasswordReset
        {
            get { return enableReset; }
        }

        public override bool EnablePasswordRetrieval
        {
            get { return enableRetrieve; }
        }

        public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            var users = RetrieveUsers(false);
            var userCollection = new List<Data.User>();

            foreach (var user in users)
            {
                if (string.Compare(emailToMatch, user.Email, true, CultureInfo.CurrentCulture) == 0)
                {
                    userCollection.Add(user);
                }
            }

            totalRecords = userCollection.Count;
            return CreateMembershipUserCollection(userCollection.GetRange(pageIndex*pageSize, (totalRecords < pageSize ? totalRecords : pageSize)));
        }

        public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords)
        {
            var users = RetrieveUsers(false);
            var userCollection = new List<Data.User>();

            foreach (var user in users)
            {
                if (string.Compare(usernameToMatch, user.UserName, true, CultureInfo.CurrentCulture) == 0)
                {
                    userCollection.Add(user);
                }
            }

            totalRecords = userCollection.Count;
            return CreateMembershipUserCollection(userCollection.GetRange(pageIndex*pageSize, (totalRecords < pageSize ? totalRecords : pageSize)));
        }

        public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords)
        {
            var users = RetrieveUsers(false);
            totalRecords = users.Count;
            var userList = new List<Data.User>(users);
            return CreateMembershipUserCollection(userList.GetRange(pageIndex*pageSize, (totalRecords < pageSize ? totalRecords : pageSize)));
        }

        public override int GetNumberOfUsersOnline()
        {
            var onlineCounter = 0;
            var minutes = Membership.UserIsOnlineTimeWindow;
            var users = RetrieveUsers(true);

            foreach (var user in users)
            {
                if (user.LastActivity > DateTime.UtcNow.AddMinutes(-minutes))
                {
                    onlineCounter++;
                }
            }

            return onlineCounter;
        }

        public override string GetPassword(string username, string answer)
        {
            var users = RetrieveUsers(true);
            var usr = users.FindByUserName(username);

            if (usr != null && string.Compare(usr.ChallengeAnswer, answer, true, CultureInfo.CurrentCulture) == 0)
            {
                switch (PasswordFormat)
                {
                    case MembershipPasswordFormat.Clear:
                        return encoder.GetString(usr.Password);

                    case MembershipPasswordFormat.Encrypted:
                        return encoder.GetString(base.DecryptPassword(usr.Password));
                }
            }

            throw new NotSupportedException();
        }

        public override MembershipUser GetUser(string username, bool userIsOnline)
        {
            var users = RetrieveUsers(false);
            var usr = users.FindByUserName(username);

            if (usr != null)
            {
                return usr.CreateMembershipUser(Membership.Provider.Name);
            }

            return null;
        }

        public override MembershipUser GetUser(object providerUserKey, bool userIsOnline)
        {
            var key = (Guid) providerUserKey;
            var users = RetrieveUsers(false);
            var usr = users.FindByKey(key);

            if (usr != null)
            {
                if (userIsOnline)
                {
                    usr.LastActivity = DateTime.UtcNow;
                    users.Save(DataStoreFile);
                }

                return usr.CreateMembershipUser(Membership.Provider.Name);
            }

            return null;
        }

        public override string GetUserNameByEmail(string email)
        {
            var users = RetrieveUsers(false);
            var usr = users.FindByEmail(email);

            if (usr != null)
            {
                return usr.UserName;
            }

            return null;
        }

        public override int MaxInvalidPasswordAttempts
        {
            get { return attempts; }
        }

        public override int MinRequiredNonAlphanumericCharacters
        {
            get { return minNonAlphaChars; }
        }

        public override int MinRequiredPasswordLength
        {
            get { return minPasswordLength; }
        }

        public override int PasswordAttemptWindow
        {
            get { return attemptWindow; }
        }

        public override MembershipPasswordFormat PasswordFormat
        {
            get { return passwordFormat; }
        }

        public override string PasswordStrengthRegularExpression
        {
            get { return passwordExpression; }
        }

        public override bool RequiresQuestionAndAnswer
        {
            get { return requiresChallenge; }
        }

        public override bool RequiresUniqueEmail
        {
            get { return uniqueEmail; }
        }

        public override string ResetPassword(string username, string answer)
        {
            var users = RetrieveUsers(false);
            var user = users.FindByUserName(username);

            var password = Membership.GeneratePassword(minPasswordLength, minNonAlphaChars);
            var newPassword = encoder.GetBytes(password);

            if (user != null)
            {
                switch (PasswordFormat)
                {
                    case MembershipPasswordFormat.Clear:
                        user.Password = newPassword;
                        break;

                    case MembershipPasswordFormat.Encrypted:
                        user.Password = base.EncryptPassword(newPassword);
                        break;

                    case MembershipPasswordFormat.Hashed:
                        user.Password = ComputeHash(newPassword);
                        break;
                }

                users.Save(DataStoreFile);
                return password;
            }

            return null;
        }

        public override bool UnlockUser(string userName)
        {
            var users = RetrieveUsers(false);
            var usr = users.FindByUserName(userName);

            if (usr != null)
            {
                usr.IsLocked = false;
                users.Save(DataStoreFile);
            }

            return true;
        }

        public override void UpdateUser(MembershipUser user)
        {
            Throw.IfNull(user, "user");

            var users = RetrieveUsers(false);
            var storedUser = users.FindByUserName(user.UserName);

            if (storedUser != null)
            {
                storedUser.LastActivity = user.LastActivityDate;
                storedUser.LastLockedOut = user.LastLockoutDate;
                storedUser.LastPasswordChange = user.LastPasswordChangedDate;
                storedUser.ChallengeQuestion = user.PasswordQuestion;
                storedUser.Email = user.Email;
                storedUser.IsLocked = user.IsLockedOut;
                storedUser.IsActive = user.IsApproved;
                storedUser.Comment = user.Comment;

                users.Save(DataStoreFile);
            }
        }

        public override bool ValidateUser(string username, string password)
        {
            var users = RetrieveUsers(false);
            var usr = users.FindByUserName(username);
            var passwordBytes = encoder.GetBytes(password);
            var passed = false;

            if (usr != null)
            {
                switch (PasswordFormat)
                {
                    case MembershipPasswordFormat.Clear:
                        passed = CompareByteArray(usr.Password, passwordBytes);
                        break;

                    case MembershipPasswordFormat.Encrypted:
                        passed = CompareByteArray(usr.Password, DecryptPassword(passwordBytes));
                        break;

                    case MembershipPasswordFormat.Hashed:
                        passed = CompareByteArray(usr.Password, ComputeHash(passwordBytes));
                        break;
                }
            }

            if (passed)
            {
                if (usr.IsLocked)
                {
                    passed = false;
                }

                else
                {
                    usr.LastLoggedIn = DateTime.UtcNow;
                    usr.LastActivity = DateTime.UtcNow;
                    users.Save(DataStoreFile);
                }
            }

            return passed;
        }
    }
}